package morphia.spring;

import com.google.code.morphia.Morphia;
import com.google.code.morphia.Datastore;
import com.google.code.morphia.DatastoreImpl;
import com.google.code.morphia.dao.BasicDAO;
import com.google.code.morphia.dao.DAO;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import com.mongodb.MongoOptions;
import com.mongodb.ServerAddress;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
 * Injector processor.<br/>
 * Usage: add a bean into your spring context<div style="color:#515151">
 * &nbsp;&nbsp;&nbsp;&nbsp;&lt;bean class="morphia.spring.MorphiaAnnotationBeanPostProcessor"/&gt;
 * </div>
 * @author Kent Yeh
 */
public class MorphiaAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements Ordered,
        ApplicationContextAware {

    private static Logger logger = LoggerFactory.getLogger(MorphiaAnnotationBeanPostProcessor.class);
    private ApplicationContext context;
    private ConfigurableApplicationContext regContext;
    private Morphia morphia = null;
    private static AtomicInteger defcnt = new AtomicInteger(0);
    private Map<String, FieldHolder> fh = new TreeMap<String, FieldHolder>();
    private Map<String, MethodHolder> mh = new TreeMap<String, MethodHolder>();
    private String mongoPrefix = "A";
    private String datastorePrefix = "B";
    private String daoPrefix = "C";
    private Set<Integer> capset = new HashSet<Integer>();
    private Set<Integer> idxset = new HashSet<Integer>();

    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.context = applicationContext;
        if (applicationContext instanceof ConfigurableApplicationContext) {
            regContext = (ConfigurableApplicationContext) applicationContext;
        }
    }

    public <T> T getBean(String name, Class<T> requiredType) {
        try {
            return context.getBean(name, requiredType);
        } catch (NoSuchBeanDefinitionException ex) {
            logger.warn("No bean name '{}'  of {} is defined.", name, requiredType.getSimpleName());
            return null;
        } catch (BeansException ex) {
            logger.warn(String.format("Can't get %s[%s] bean.", requiredType.getSimpleName(), name), ex);
            return null;
        }
    }

    private void registerBean(String name, Object bean) {
        if (regContext != null) {
            regContext.getBeanFactory().registerSingleton(name, bean);
            logger.debug("register {} with {}->{}", new Object[]{name, bean.getClass(), bean});
        }
    }

    private String getCamelName(Class<?> clazz) {
        String name = clazz.getSimpleName();
        return name.substring(0, 1).toLowerCase() + name.substring(1);
    }

    private Mongo getMongo(DBDef mongoDef) throws BeansException {
        Mongo mongo = getBean(mongoDef.host() + '_' + mongoDef.port(), Mongo.class);
        if (mongo == null) {
            try {
                MongoOptions options = StringUtils.hasText(mongoDef.optionsRef()) ? getBean(mongoDef.optionsRef(), MongoOptions.class) : null;
                mongo = options == null ? new Mongo(mongoDef.host(), mongoDef.port())
                        : new Mongo(new ServerAddress(mongoDef.host(), mongoDef.port()), options);
                registerBean(mongoDef.host() + '_' + mongoDef.port(), mongo);
            } catch (UnknownHostException ex) {
                throw new BeanCreationException(ex.getMessage(), ex);
            } catch (MongoException ex) {
                throw new BeanCreationException(ex.getMessage(), ex);
            }
        }
        return mongo;
    }

    private Mongo getMongo(DbDefinition mongoDef) throws BeansException {
        Mongo mongo = getBean(mongoDef.getHost() + '_' + mongoDef.getPort(), Mongo.class);
        if (mongo == null) {
            try {
                MongoOptions options = StringUtils.hasText(mongoDef.getOptionsRef()) ? getBean(mongoDef.getOptionsRef(), MongoOptions.class) : null;
                mongo = options == null ? new Mongo(mongoDef.getHost(), mongoDef.getPort())
                        : new Mongo(new ServerAddress(mongoDef.getHost(), mongoDef.getPort()), options);
                registerBean(mongoDef.getHost() + '_' + mongoDef.getPort(), mongo);
            } catch (UnknownHostException ex) {
                throw new BeanCreationException(ex.getMessage(), ex);
            } catch (MongoException ex) {
                throw new BeanCreationException(ex.getMessage(), ex);
            }
        }
        return mongo;
    }

    private Morphia getMophia(MorphiaDef morphiaDef) {
        if (morphia == null) {
            if (morphiaDef.mapClass().length > 0) {
                HashSet<Class> clazzSet = new HashSet<Class>();
                clazzSet.addAll(Arrays.asList(morphiaDef.mapClass()));
                morphia = new Morphia(clazzSet);
            } else {
                morphia = new Morphia();
            }
            for (String pkg : morphiaDef.value()) {
                morphia.mapPackage(pkg, true);
            }
            for (String pkg : morphiaDef.mapPackages()) {
                morphia.mapPackage(pkg, true);
            }
        } else {
            if (morphiaDef.mapClass().length > 0) {
                for (Class<?> clazz : morphiaDef.mapClass()) {
                    if (!morphia.isMapped(clazz)) {
                        morphia.map(clazz);
                    }
                }
            }
            if (morphiaDef.mapPackages().length > 0) {
                for (String pkg : morphiaDef.mapPackages()) {
                    morphia.mapPackage(pkg);
                }
            }
        }
        return morphia;
    }

    private Datastore getDataStore(DBDef dsDef) {
        String iden = new StringBuilder(dsDef.host()).append('_').
                append(dsDef.port()).append('_').append(dsDef.databaseName()).toString();
        Datastore ds = getBean(iden, Datastore.class);
        if (ds == null) {
            if (morphia == null) {
                morphia = new Morphia();
            }
            ds = new DatastoreImpl(morphia, getMongo(dsDef), dsDef.databaseName(),
                    StringUtils.hasText(dsDef.username()) ? dsDef.username() : null,
                    StringUtils.hasText(dsDef.username()) ? StringUtils.hasText(dsDef.password()) ? dsDef.password().toCharArray()
                    : new char[]{} : null);
            registerBean(iden, ds);
        }
        return ds;
    }

    private Datastore getDataStore(DbDefinition dsDef, boolean isCpas) {
        String iden = new StringBuilder(dsDef.getHost()).append('_').
                append(dsDef.getPort()).append('_').append(dsDef.getDatabaseName()).toString();
        Datastore ds = getBean(iden, Datastore.class);
        if (ds == null) {
            if (morphia == null) {
                morphia = new Morphia();
            }
            ds = new DatastoreImpl(morphia, getMongo(dsDef), dsDef.getDatabaseName(),
                    StringUtils.hasText(dsDef.getUsername()) ? dsDef.getUsername() : null,
                    StringUtils.hasText(dsDef.getUsername()) ? StringUtils.hasText(dsDef.getPassword()) ? dsDef.getPassword().toCharArray()
                    : new char[]{} : null);
            registerBean(iden, ds);
        }
        if (isCpas && !capset.contains(ds.hashCode())) {
            capset.add(ds.hashCode());
            ds.ensureCaps();
        }
        return ds;
    }

    @Override
    public boolean postProcessAfterInstantiation(final Object bean, String beanName) throws BeansException {
        ReflectionUtils.doWithFields(bean.getClass(), new ReflectionUtils.FieldCallback() {

            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
                //process DBDef
                DBDef dbdef = field.getAnnotation(DBDef.class);
                if (dbdef != null) {
                    DbDefinition dbd = getBean(DbDefinitionImpl.getDefaultDefName(dbdef), DbDefinition.class);
                    if (dbd == null) {
                        Mongo mongo = getMongo(dbdef);
                        Datastore ds = getDataStore(dbdef);
                        dbd = new DbDefinitionImpl(dbdef);
                        registerBean(DbDefinitionImpl.getDefaultDefName(dbdef), dbd);
                    }
                    ReflectionUtils.makeAccessible(field);
                    field.set(bean, dbd);
                }
                //Injecct Mongo
                MongoDef mongoDef = field.getAnnotation(MongoDef.class);
                if (mongoDef != null) {
                    DbDefinition dbd = getBean(mongoDef.value(), DbDefinition.class);
                    if (dbd == null) {
                        fh.put(String.format("%s%03d", mongoPrefix, defcnt.getAndAdd(1)), new FieldHolder(bean, field));
                    } else {
                        ReflectionUtils.makeAccessible(field);
                        field.set(bean, getMongo(dbd));
                    }
                }
                //Inject Mophia
                MorphiaDef morphiaDef = field.getAnnotation(MorphiaDef.class);
                if (morphiaDef != null) {
                    ReflectionUtils.makeAccessible(field);
                    field.set(bean, getMophia(morphiaDef));
                }
                //Inject DataStore
                DatastoreDef dsDef = field.getAnnotation(DatastoreDef.class);
                if (dsDef != null) {
                    DbDefinition dbd = getBean(dsDef.value(), DbDefinition.class);
                    if (dbd == null) {
                        fh.put(String.format("%s%03d", datastorePrefix, defcnt.getAndAdd(1)), new FieldHolder(bean, field));
                    } else {
                        ReflectionUtils.makeAccessible(field);
                        field.set(bean, getDataStore(dbd, dsDef.ensureCaps()));
                    }
                }
                //Inject Dao
                DaoDef daoDef = field.getAnnotation(DaoDef.class);
                if (daoDef != null) {
                    fh.put(String.format("%s%03d", daoPrefix, defcnt.getAndAdd(1)), new FieldHolder(bean, field));
                }
            }
        });
        ReflectionUtils.doWithMethods(bean.getClass(), new ReflectionUtils.MethodCallback() {

            public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
                //process DBDef
                DBDef dbdef = method.getAnnotation(DBDef.class);
                if (dbdef != null) {
                    DbDefinition dbd = getBean(DbDefinitionImpl.getDefaultDefName(dbdef), DbDefinition.class);
                    if (dbd == null) {
                        Mongo mongo = getMongo(dbdef);
                        Datastore ds = getDataStore(dbdef);
                        dbd = new DbDefinitionImpl(dbdef);
                        registerBean(DbDefinitionImpl.getDefaultDefName(dbdef), dbd);
                    }
                    try {
                        method.invoke(bean, dbdef);
                    } catch (InvocationTargetException ex) {
                        throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                append("(DbDefinition) failed: ").toString(), ex);
                    }
                }
                //process Mongo
                MongoDef mongoDef = method.getAnnotation(MongoDef.class);
                if (mongoDef != null) {
                    DbDefinition dbd = getBean(mongoDef.value(), DbDefinition.class);
                    if (dbd == null) {
                        mh.put(String.format("%s%03d", mongoPrefix, defcnt.getAndAdd(1)), new MethodHolder(bean, method));
                    } else {
                        ReflectionUtils.makeAccessible(method);
                        try {
                            method.invoke(bean, getMongo(dbd));
                        } catch (InvocationTargetException ex) {
                            throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                    append("(Mongo) failed: ").toString(), ex);
                        }
                    }
                }
                //process Mophia
                MorphiaDef morphiaDef = method.getAnnotation(MorphiaDef.class);
                if (morphiaDef != null) {
                    ReflectionUtils.makeAccessible(method);
                    try {
                        method.invoke(bean, getMophia(morphiaDef));
                    } catch (InvocationTargetException ex) {
                        throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                append("(Morphia) failed: ").toString(), ex);
                    }
                }
                //process DataStore
                DatastoreDef dsDef = method.getAnnotation(DatastoreDef.class);
                if (dsDef != null) {
                    DbDefinition dbd = getBean(dsDef.value(), DbDefinition.class);
                    if (dbd == null) {
                        mh.put(String.format("%s%03d", datastorePrefix, defcnt.getAndAdd(1)), new MethodHolder(bean, method));
                    } else {
                        ReflectionUtils.makeAccessible(method);
                        try {
                            method.invoke(bean, getDataStore(dbdef));
                        } catch (InvocationTargetException ex) {
                            throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                    append("(Datastore) failed: ").toString(), ex);
                        }
                    }
                }
                //process Dao
                DaoDef daoDef = method.getAnnotation(DaoDef.class);
                if (daoDef != null) {
                    mh.put(String.format("%s%03d", daoPrefix, defcnt.getAndAdd(1)), new MethodHolder(bean, method));
                }
            }
        });
        try {
            processHolder();
        } catch (Exception e) {
            throw new BeanCreationException("Process failed", e);
        }
        return true;
    }

    private synchronized void processHolder() throws IllegalArgumentException, IllegalAccessException {
        Set<String> waitForGenDao = new HashSet<String>();
        if (fh.size() > 0) {
            String[] keys = new String[fh.size()];
            Map<String, FieldHolder> map = Collections.synchronizedMap(fh);
            map.keySet().toArray(keys);
            for (String key : keys) {
                FieldHolder f = fh.get(key);
                if (f != null) {
                    Field field = f.getField();
                    if (key.startsWith(mongoPrefix)) {
                        DbDefinition dbd = getBean(field.getAnnotation(MongoDef.class).value(), DbDefinition.class);
                        if (dbd != null) {
                            ReflectionUtils.makeAccessible(field);
                            f.getField().set(f.getBean(), getMongo(dbd));
                            map.remove(key);
                        }
                    } else if (key.startsWith(datastorePrefix)) {
                        DatastoreDef dsDef = field.getAnnotation(DatastoreDef.class);
                        DbDefinition dbd = getBean(dsDef.value(), DbDefinition.class);
                        if (dbd != null) {
                            ReflectionUtils.makeAccessible(field);
                            field.set(f.getBean(), getDataStore(dbd, dsDef.ensureCaps()));
                            map.remove(key);
                        }
                    } else if (key.startsWith(daoPrefix)) {
                        DaoDef daoDef = field.getAnnotation(DaoDef.class);
                        DAO dao = StringUtils.hasLength(daoDef.name()) ? getBean(daoDef.name(), DAO.class) : null;
                        DbDefinition dbd = getBean(daoDef.value(), DbDefinition.class);
                        if (dao == null && dbd != null) {
                            if (ClassUtils.isAssignable(field.getType(), BasicDAO.class)) {
                                Class<?> genericType = Object.class.equals(daoDef.clazz()) ? null : daoDef.clazz();
                                if (genericType == null) {
                                    if (field.getGenericType() instanceof ParameterizedType) {
                                        ParameterizedType pt = (ParameterizedType) field.getGenericType();
                                        Type[] typeArguments = pt.getActualTypeArguments();
                                        if (typeArguments.length == 2) {
                                            genericType = (Class<?>) typeArguments[0];
                                        } else {
                                            throw new IllegalArgumentException(new StringBuilder(
                                                    "@DaoDef annotation should assoicate a Generic ParameterizedType like com.google.code.morphia.dao.DAO<Type,Key> ").append(field.getName()).append(" or Define @DaoDef(clazz=\"Type\",...)").toString());
                                        }
                                    } else {
                                        throw new IllegalArgumentException(new StringBuilder(
                                                "@DaoDef annotation should assoicate a Generic ParameterizedType like com.google.code.morphia.dao.DAO<Type,Key> ").append(field.getName()).append(" or Define @DaoDef(clazz=\"Type\",...)").toString());
                                    }
                                }
                                String regName = StringUtils.hasText(daoDef.name()) ? daoDef.name() : getCamelName(genericType) + "Dao";
                                dao = getBean(regName, DAO.class);
                                if (dao == null) {
                                    dao = new BasicDAO(genericType, getDataStore(dbd, false));
                                    String daoName = StringUtils.hasLength(daoDef.name()) ? daoDef.name() : regName;
                                    registerBean(daoName, dao);
                                }
                            } else {
                                String regName = StringUtils.hasText(daoDef.name()) ? daoDef.name() : getCamelName(field.getType()) + "Dao";
                                dao = getBean(regName, DAO.class);
                                if (dao == null) {
                                    BeanDefinitionRegistry registry = (BeanDefinitionRegistry) regContext.getBeanFactory();
                                    GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                                    beanDefinition.setBeanClass(field.getType());
                                    beanDefinition.setLazyInit(false);
                                    beanDefinition.setAbstract(false);
                                    beanDefinition.setAutowireCandidate(true);
                                    beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
                                    beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, getDataStore(dbd, false));
                                    registry.registerBeanDefinition(regName, beanDefinition);
                                    waitForGenDao.add(regName);
                                }
                            }
                        }
                        if (dao != null) {
                            ReflectionUtils.makeAccessible(field);
                            field.set(f.getBean(), dao);
                            if (daoDef.ensureIndexes() && !idxset.contains(dao.hashCode())) {
                                dao.ensureIndexes();
                                idxset.add(dao.hashCode());
                            }
                            map.remove(key);
                        }
                    }
                }
            }
        }
        if (mh.size() > 0) {
            String[] keys = new String[fh.size()];
            Map<String, MethodHolder> map = Collections.synchronizedMap(mh);
            map.keySet().toArray(keys);
            for (String key : keys) {
                MethodHolder m = mh.get(key);
                if (m != null) {
                    Method method = m.getMethod();
                    if (key.startsWith(mongoPrefix)) {
                        MongoDef mongoDef = method.getAnnotation(MongoDef.class);
                        if (mongoDef != null) {
                            DbDefinition dbd = getBean(mongoDef.value(), DbDefinition.class);
                            if (dbd != null) {
                                ReflectionUtils.makeAccessible(method);
                                try {
                                    method.invoke(m.getBean(), getMongo(dbd));
                                    map.remove(key);
                                } catch (InvocationTargetException ex) {
                                    throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                            append("(Mongo) failed: ").toString(), ex);
                                }
                            }
                        }
                    } else if (key.startsWith(datastorePrefix)) {
                        DatastoreDef dsDef = method.getAnnotation(DatastoreDef.class);
                        DbDefinition dbd = getBean(dsDef.value(), DbDefinition.class);
                        if (dbd != null) {
                            ReflectionUtils.makeAccessible(method);
                            try {
                                method.invoke(m.getBean(), getDataStore(dbd, dsDef.ensureCaps()));
                                map.remove(key);
                            } catch (InvocationTargetException ex) {
                                throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                        append("(Datastore) failed: ").toString(), ex);
                            }
                        }
                    } else if (key.startsWith(daoPrefix)) {
                        DaoDef daoDef = method.getAnnotation(DaoDef.class);
                        DAO dao = StringUtils.hasLength(daoDef.name()) ? getBean(daoDef.name(), DAO.class) : null;
                        DbDefinition dbd = getBean(daoDef.value(), DbDefinition.class);
                        if (dao == null && dbd != null) {
                            Class<?> parmatypes[] = method.getParameterTypes();
                            Class<?> pt = parmatypes.length > 0 ? parmatypes[0] : null;
                            if (ClassUtils.isAssignable(pt, BasicDAO.class)) {
                                Class<?> genericType = Object.class.equals(daoDef.clazz()) ? null : daoDef.clazz();
                                if (genericType == null) {
                                    Type[] genericParameterTypes = method.getGenericParameterTypes();
                                    if (genericParameterTypes.length > 0 && genericParameterTypes[0] instanceof ParameterizedType) {
                                        ParameterizedType aType = (ParameterizedType) genericParameterTypes[0];
                                        Type[] parameterArgTypes = aType.getActualTypeArguments();
                                        if (parameterArgTypes.length == 2) {
                                            genericType = (Class<?>) parameterArgTypes[0];
                                        } else {
                                            throw new IllegalArgumentException(new StringBuilder(
                                                    "@DaoDef annotation should assoicate a Generic ParameterizedType like ").append(method.getName()).append("(com.google.code.morphia.dao.DAO<Type,Key>)").append(" or Define @DaoDef(clazz=\"Type\",...)").toString());
                                        }
                                    } else {
                                        throw new IllegalArgumentException(new StringBuilder(
                                                "@DaoDef annotation should assoicate a Generic ParameterizedType like ").append(method.getName()).append("(com.google.code.morphia.dao.DAO<Type,Key>)").append(" or Define @DaoDef(clazz=\"Type\",...)").toString());
                                    }
                                }
                                String regName = StringUtils.hasText(daoDef.name()) ? daoDef.name() : getCamelName(genericType) + "Dao";
                                dao = getBean(regName, DAO.class);
                                if (dao == null) {
                                    dao = new BasicDAO(genericType, getDataStore(dbd, false));
                                    String daoName = StringUtils.hasLength(daoDef.name()) ? daoDef.name() : regName;
                                    registerBean(daoName, dao);
                                }
                            } else {
                                String regName = StringUtils.hasText(daoDef.name()) ? daoDef.name() : getCamelName(pt) + "Dao";
                                dao = getBean(regName, DAO.class);
                                if (dao == null) {
                                    BeanDefinitionRegistry registry = (BeanDefinitionRegistry) regContext.getBeanFactory();
                                    GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
                                    beanDefinition.setBeanClass(pt);
                                    beanDefinition.setLazyInit(false);
                                    beanDefinition.setAbstract(false);
                                    beanDefinition.setAutowireCandidate(true);
                                    beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON);
                                    beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, getDataStore(dbd, false));
                                    registry.registerBeanDefinition(regName, beanDefinition);
                                    waitForGenDao.add(regName);
                                }
                            }
                        }
                        if (dao != null) {
                            ReflectionUtils.makeAccessible(method);
                            try {
                                method.invoke(m.getBean(), dao);
                                if (daoDef.ensureIndexes() && !idxset.contains(dao.hashCode())) {
                                    dao.ensureIndexes();
                                    idxset.add(dao.hashCode());
                                }
                                map.remove(key);
                            } catch (InvocationTargetException ex) {
                                throw new IllegalArgumentException(new StringBuilder("Invoke void ").append(method.getName()).
                                        append("(param extends DAO<Type,Key>) failed: ").toString(), ex);
                            }
                        }
                    }
                }
            }
        }
        if (waitForGenDao.size() > 0) {
            for (String regName : waitForGenDao) {
                context.getBean(regName);
            }
        }
    }

    private class FieldHolder {

        Object bean;
        Field field;

        public FieldHolder(Object bean, Field field) {
            this.bean = bean;
            this.field = field;
        }

        public Object getBean() {
            return bean;
        }

        public Field getField() {
            return field;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final FieldHolder other = (FieldHolder) obj;
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 73 * hash + (this.bean != null ? this.bean.hashCode() : 0);
            hash = 73 * hash + (this.field != null ? this.field.hashCode() : 0);
            return hash;
        }
    }

    private class MethodHolder {

        Object bean;
        Method method;

        public MethodHolder(Object bean, Method method) {
            this.bean = bean;
            this.method = method;
        }

        public Object getBean() {
            return bean;
        }

        public Method getMethod() {
            return method;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final MethodHolder other = (MethodHolder) obj;
            return true;
        }

        @Override
        public int hashCode() {
            int hash = 7;
            hash = 89 * hash + (this.bean != null ? this.bean.hashCode() : 0);
            hash = 89 * hash + (this.method != null ? this.method.hashCode() : 0);
            return hash;
        }
    }
}
